Skip to content

feat(phase8 #92 D8.5-BE): canonical UIMessage chat history + runtime_kind discriminator#1706

Merged
earayu merged 1 commit into
mainfrom
bryce/phase8-task92-d85-be-non-agent-uimessage
Apr 25, 2026
Merged

feat(phase8 #92 D8.5-BE): canonical UIMessage chat history + runtime_kind discriminator#1706
earayu merged 1 commit into
mainfrom
bryce/phase8-task92-d85-be-non-agent-uimessage

Conversation

@earayu
Copy link
Copy Markdown
Collaborator

@earayu earayu commented Apr 25, 2026

Summary

Phase 8 task #92 (D8.5-BE) — first-cut backend migration for non-agent bot path. Per architect msg=01918929 + Weston msg=df87fe24 + earayu2 msg=f20d5034 hard-cut, scope locked at A+B+C only. The translator extension and legacy-class deletion are explicitly deferred (architect canonical: don't build dead-code for a non-agent live path that doesn't exist in current code; let the future feature task handle that, and #80 owns the deletion).

A. runtime_kind discriminator on agent_message

  • ORM column on AgentMessage with values agent_runtime / direct_chat / rag_chat (mutually exclusive enum)
  • Server-default agent_runtime so existing rows backfill (no data migration step)
  • role keeps speaker semantics (user/assistant/system) independent of runtime kind, per Weston msg=94dac98a
  • Alembic migration c8f2d34a51e7 (down_revision=84fac9e3d8c2) — additive; downgrade drops the column

B. ChatService._build_v3_chat_history rewrite

  • Returns list[AgentTurnSnapshot] (one snapshot per assistant turn) instead of legacy list[list[ChatMessage]]
  • Reuses snapshot_assembler.assemble_parts_from_artifacts (chore: fix quantize failed #90 D8.4d projection) so historical turns expose the same UIMessagePart shape the FE consumes from the live SSE stream — D8 §2 wire/at-rest byte-equal
  • error_text for FAILED / CANCELLED turns surfaces an error_summary artifact's message, falling back to turn.error_message (mirrors snapshot endpoint contract from chore: fix quantize failed #90)
  • Turn's user query exposed via input_text on the snapshot envelope so user/assistant render from a single object per turn
  • Legacy helpers (_extract_artifact_text / _extract_references / _map_reference_item / _artifact_type_value / _coerce_timestamp) retired alongside the legacy shape

C. ChatDetails.history schema

  • Type changed to Optional[list[AgentTurnSnapshot]]
  • Cycle break: conversation.schemasagent_runtime.uimessageagent_runtime.schemasconversation.schemas resolved via TYPE_CHECKING import + module-level ChatDetails.model_rebuild() hook
  • AgentTurnSnapshot gains runtime_kind (default "agent_runtime") and input_text so live + historical share the same envelope shape; TurnService.get_turn_snapshot writes both fields on the live snapshot endpoint

D. Deferred (out of #92 scope)

  • Translator extension for chat.text.delta / chat.completed envelope types — non-agent live SSE path doesn't exist; reintroducing it is a feature task
  • StoredChatMessagePart / RedisChatMessageHistory deletion — Weston msg=df87fe24 / PM msg=01918929 → owned by [Features] on premise deployment #80 post-soak

Tests

  • NEW test_get_chat_returns_canonical_uimessage_history pins the new shape (snapshot per turn with text + source-url + data-citation parts, runtime_kind, input_text, timeline_cursor)
  • NEW test_get_chat_history_surfaces_error_text_for_failed_turn pins error_text contract for FAILED turns (artifact-preferred → turn.error_message fallback)
  • NEW test_get_chat_history_does_not_expose_legacy_chatmessage_shape regression-guard against revert to list[list[ChatMessage]]
  • test_agent_runtime_v3.py import path updated (AgentTurnSnapshot now imported from uimessage directly; back-compat re-export through schemas retired to break cycle)

Per D10 §G hard gate 1 (comprehensive grep sweep) ran across aperag/ + tests/unit_test/ + tests/e2e_http/hurl/ + tests/e2e_http/scripts/. Only web/src/components/chat/chat-messages.tsx reads chat.history in the old shape — that's the explicit hand-off seam for #93 huangheng (per architect msg=6e53a7c4).

Hand-off for #93 huangheng (FE)

Per huangheng's questions in msg=4017bc0c:

  1. ChatDetails.history shape: list[AgentTurnSnapshot] (each snapshot has turn_id / chat_id / runtime_kind / role: "assistant" / status / parts: UIMessagePart[] / error_text? / timeline_cursor / input_text / timestamps)
  2. getBotChat endpoint: unchanged — GET /api/v2/bots/{bot_id}/chats/{chat_id} still returns ChatDetails
  3. Snapshot endpoint reuse: yes — non-agent uses the same AgentTurnSnapshot shape (now with runtime_kind discriminator). The endpoint stays at /api/v2/agent/chats/{cid}/turns/{tid} for now; a non-agent specific endpoint is not needed because the production non-agent live path doesn't exist
  4. runtime_kind in FE: surfaced for transparency but FE need not branch on it for [Features] store the celery task result to a backend #93 — historical render uses parts directly
  5. Each historical turn has turn_id: yes, from agent_turn table (existing)

Test plan

  • pytest tests/unit_test/agent_runtime/ tests/unit_test/chat/ -q → 139 passed
  • pytest tests/unit_test --deselect concurrent_control flake -q833 passed / 29 skip / 0 fail
  • ruff check → clean
  • ruff format --check → clean (448 files)
  • e2e-http-smoke + e2e-http-provider — pending CI
  • FE typed schema regen (yarn api:v2:types) — yarn not available in BE worktree; [Features] store the celery task result to a backend #93 huangheng will regen as part of FE migration
  • CR — pending Opus reviewer per RR2 single-Opus-CR

Phase 8 D8.x Phase A + B + #90 + #92 status

Task Status
#73 D8.1 wire emitter ✅ merged 51137301
#74 D8.2 at-rest storage ✅ merged e290488b
#75 D8.3 tool/citation/consent ✅ merged bd4052d5
#76 D8.4a SDK transport ✅ merged 63a9d522
#77 D8.4b parts renderer ✅ merged f0351662
#78 D8.4c interactive consent UI ✅ merged 3195d18b
#90 D8.4d snapshot canonical parts ✅ merged 3f9303cd
#92 D8.5-BE this PR
#93 D8.5-FE huangheng, awaits this PR's first-cut
#80 D8.6 cleanup parked, post-soak

🤖 Generated with Claude Code

…kind discriminator

Phase 8 task #92 (D8.5-BE) — first-cut backend migration of the
non-agent chat path to the canonical ``UIMessage`` shape, scoped per
architect msg=01918929 + Weston msg=df87fe24 + earayu2 msg=f20d5034
hard-cut acceptance:

The inventory revealed the production "non-agent chat path" the
original D8.5 design assumed has already converged on the agent
runtime (``chat_completion_service.openai_chat_completions`` already
delegates to ``runtime_manager.turn_service.create_or_get_turn`` and
``ChatService.create_chat`` rejects non-AGENT bots). So the actual
#92 work is A+B+C only — adding the discriminator column for future
non-agent paths and migrating the user-visible chat history shape to
canonical UIMessage. The translator extension (``chat.text.delta`` /
``chat.completed``) and the ``StoredChatMessagePart`` /
``RedisChatMessageHistory`` deletion are deferred per architect /
Weston canonical lock.

Changes:

A. ``runtime_kind`` discriminator on ``agent_message`` table
- ``aperag/domains/agent_runtime/db/models.py``: new
  ``runtime_kind: str`` ORM column with values
  ``agent_runtime`` / ``direct_chat`` / ``rag_chat`` (mutually
  exclusive enum); existing rows backfill via
  ``server_default="agent_runtime"``. ``role`` keeps speaker
  semantics independent of the runtime that produced the message.
- ``aperag/migration/versions/...c8f2d34a51e7_add_agent_message_runtime_kind.py``:
  additive migration; downgrade drops the column.

B. ``ChatService._build_v3_chat_history`` rewrite
- Returns ``list[AgentTurnSnapshot]`` (one snapshot per assistant
  turn) instead of the legacy ``list[list[ChatMessage]]`` shape.
- Reuses ``snapshot_assembler.assemble_parts_from_artifacts`` (the
  #90 D8.4d projection) so historical turns expose the same
  ``UIMessagePart`` shape the FE consumes from the live SSE stream
  (D8 §2 wire/at-rest byte-equal).
- ``error_text`` for FAILED / CANCELLED turns surfaces an
  ``error_summary`` artifact's message, falling back to
  ``turn.error_message`` — mirrors the snapshot endpoint contract.
- The turn's user query lives at ``input_text`` on the snapshot
  envelope (rather than as a separate ``role=human`` ChatMessage)
  so the FE renders user/assistant from a single object per turn.
- Legacy ``_extract_artifact_text`` / ``_extract_references`` /
  ``_map_reference_item`` / ``_artifact_type_value`` /
  ``_coerce_timestamp`` helpers are retired alongside the legacy
  shape.

C. ``ChatDetails.history`` schema
- ``aperag/domains/conversation/schemas.py``: ``history`` is now
  ``Optional[list[AgentTurnSnapshot]]`` with explicit description
  citing D8 §2 byte-equal canonical and the new shape.
- The ``conversation.schemas`` ↔ ``agent_runtime.uimessage``
  ↔ ``agent_runtime.schemas`` ↔ ``conversation.schemas`` cycle is
  broken via ``TYPE_CHECKING`` import + a module-level
  ``ChatDetails.model_rebuild()`` hook at the bottom of
  ``conversation/schemas.py``. Pydantic resolves the forward ref at
  load time so the OpenAPI schema is fully populated.
- ``aperag/domains/agent_runtime/uimessage.py``: ``AgentTurnSnapshot``
  gains ``runtime_kind: RuntimeKind`` (default ``"agent_runtime"``)
  and ``input_text: Optional[str]`` so historical turns can render
  the user query without a separate envelope round-trip.
- ``TurnService.get_turn_snapshot`` writes both new fields on the
  live snapshot endpoint so live and historical reload paths match.

D. (deferred) Translator extension for ``chat.text.delta`` /
``chat.completed`` and ``StoredChatMessagePart`` /
``RedisChatMessageHistory`` deletion stay out of #92 per
Weston msg=df87fe24 / PM msg=01918929. The non-agent live path the
extension would have served does not exist in the current
codebase; reintroducing it is a feature task, not a refactor.

Tests:
- ``tests/unit_test/chat/test_chat_service.py`` rewritten:
  * ``test_get_chat_returns_canonical_uimessage_history`` pins the
    new shape (snapshot per turn with text + source-url +
    data-citation parts, runtime_kind, input_text)
  * ``test_get_chat_history_surfaces_error_text_for_failed_turn``
    pins the error_text contract for FAILED turns
  * ``test_get_chat_history_does_not_expose_legacy_chatmessage_shape``
    regression-guard against revert to ``list[list[ChatMessage]]``
- ``tests/unit_test/agent_runtime/test_agent_runtime_v3.py`` updated
  to import ``AgentTurnSnapshot`` from ``agent_runtime.uimessage``
  (the back-compat re-export through ``agent_runtime.schemas`` was
  retired to break the new cycle).

Per D10 §G hard gate 1 (comprehensive grep sweep) ran across
``aperag/`` + ``tests/unit_test/`` + ``tests/e2e_http/hurl/`` +
``tests/e2e_http/scripts/``: only the FE
``web/src/components/chat/chat-messages.tsx`` reads ``chat.history``
in the old shape — that is the explicit hand-off seam for #93
huangheng (per architect msg=6e53a7c4).

Gates: full unit suite 833 / 29 skip / 0 fail; ruff check + format
clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@earayu earayu merged commit b4305cf into main Apr 25, 2026
4 checks passed
@earayu earayu deleted the bryce/phase8-task92-d85-be-non-agent-uimessage branch April 25, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant